Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
17 / 17 |
CRAP | |
100.00% |
533 / 533 |
| PHP | |
100.00% |
1 / 1 |
|
100.00% |
17 / 17 |
125 | |
100.00% |
533 / 533 |
| __construct($cacheDir = null) | |
100.00% |
1 / 1 |
3 | |
100.00% |
10 / 10 |
|||
| getRenderer(Stylesheet $stylesheet) | |
100.00% |
1 / 1 |
5 | |
100.00% |
20 / 20 |
|||
| generate($xsl) | |
100.00% |
1 / 1 |
21 | |
100.00% |
115 / 115 |
|||
| fixEmptyElements(DOMNode $ir) | |
100.00% |
1 / 1 |
8 | |
100.00% |
18 / 18 |
|||
| serializeApplyTemplates(DOMNode $applyTemplates) | |
100.00% |
1 / 1 |
2 | |
100.00% |
7 / 7 |
|||
| serializeAttribute(DOMNode $attribute) | |
100.00% |
1 / 1 |
1 | |
100.00% |
7 / 7 |
|||
| serializeChildren(DOMNode $ir) | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
| serializeCloseTag(DOMNode $closeTag) | |
100.00% |
1 / 1 |
8 | |
100.00% |
35 / 35 |
|||
| serializeComment(DOMNode $comment) | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| serializeCopyOfAttributes(DOMNode $copyOfAttributes) | |
100.00% |
1 / 1 |
1 | |
100.00% |
9 / 9 |
|||
| serializeElement(DOMNode $element) | |
100.00% |
1 / 1 |
11 | |
100.00% |
41 / 41 |
|||
| serializeMatch() | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| serializeOutput(DOMNode $output) | |
100.00% |
1 / 1 |
3 | |
100.00% |
14 / 14 |
|||
| serializeSwitch(DOMNode $switch) | |
100.00% |
1 / 1 |
4 | |
100.00% |
16 / 16 |
|||
| convertAttributeValueTemplate($attrValue) | |
100.00% |
1 / 1 |
3 | |
100.00% |
9 / 9 |
|||
| convertCondition($expr) | |
100.00% |
1 / 1 |
6 | |
100.00% |
18 / 18 |
|||
| convertXPath($expr) | |
100.00% |
1 / 1 |
45 | |
100.00% |
204 / 204 |
|||
| <?php | |
| /** | |
| * @package s9e\TextFormatter | |
| * @copyright Copyright (c) 2010-2013 The s9e Authors | |
| * @license http://www.opensource.org/licenses/mit-license.php The MIT License | |
| */ | |
| namespace s9e\TextFormatter\Configurator\RendererGenerators; | |
| use DOMDocument; | |
| use DOMNode; | |
| use DOMText; | |
| use DOMXPath; | |
| use RuntimeException; | |
| use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; | |
| use s9e\TextFormatter\Configurator\Helpers\TemplateParser; | |
| use s9e\TextFormatter\Configurator\RendererGenerator; | |
| use s9e\TextFormatter\Configurator\RendererGenerators\PHP\Optimizer; | |
| use s9e\TextFormatter\Configurator\Stylesheet; | |
| /** | |
| * @see docs/DifferencesInRendering.md | |
| */ | |
| class PHP implements RendererGenerator | |
| { | |
| /** | |
| * XSL namespace | |
| */ | |
| const XMLNS_XSL = 'http://www.w3.org/1999/XSL/Transform'; | |
| /** | |
| * @var string Directory where the renderer's source is automatically saved if set, and if filepath is not set | |
| */ | |
| public $cacheDir; | |
| /** | |
| * @var string Name of the class to be created. If null, a random name will be generated | |
| */ | |
| public $className; | |
| /** | |
| * @var array Custom XPath representations as [xpath => php] | |
| */ | |
| protected $customXPath = [ | |
| // BBcodes: LIST | |
| "contains('upperlowerdecim',substring(@type,1,5))" | |
| => "strpos('upperlowerdecim',substr(\$node->getAttribute('type'),0,5))!==false", | |
| // MediaEmbed: Bandcamp | |
| '120-78*boolean(@track_id|@track_num)' | |
| => "(\$node->hasAttribute('track_id')||\$node->hasAttribute('track_num')?42:120)", | |
| // MediaEmbed: Grooveshark | |
| "substring('songWw',6-5*boolean(@songid),5)" | |
| => "(\$node->hasAttribute('songid')?'songW':'w')", | |
| "250-210*boolean(@songid)" | |
| => "(\$node->hasAttribute('songid')?40:250)", | |
| // MediaEmbed: Spotify | |
| "380-300*(contains(@uri,':track:')orcontains(@path,'/track/'))" | |
| => "(strpos(\$node->getAttribute('uri'),':track:')!==false||strpos(\$node->getAttribute('path'),'/track/')!==false?80:380)", | |
| // MediaEmbed: Twitch | |
| "substring('archl',5-4*boolean(@archive_id|@chapter_id),4)" | |
| => "(\$node->hasAttribute('archive_id')||\$node->hasAttribute('chapter_id')?'arch':'l')" | |
| ]; | |
| /** | |
| * @var string Prefix used when generating a default class name | |
| */ | |
| public $defaultClassPrefix = 'Renderer_'; | |
| /** | |
| * @var string If set, path to the file where the renderer will be saved | |
| */ | |
| public $filepath; | |
| /** | |
| * @var bool Whether to force non-void, empty elements to use the empty-element tag syntax in XML mode | |
| */ | |
| public $forceEmptyElements = true; | |
| /** | |
| * @var string Name of the last class generated | |
| */ | |
| public $lastClassName; | |
| /** | |
| * @var string Path to the last file saved | |
| */ | |
| public $lastFilepath; | |
| /** | |
| * @var Optimizer Optimizer | |
| */ | |
| public $optimizer; | |
| /** | |
| * @var string Output method | |
| */ | |
| protected $outputMethod; | |
| /** | |
| * @var string PHP source of generated renderer | |
| */ | |
| protected $php; | |
| /** | |
| * @var bool Whether to use the empty-element tag syntax with non-void elements in XML mode | |
| */ | |
| public $useEmptyElements = true; | |
| /** | |
| * @var bool Whether to use the mbstring functions as a replacement for XPath expressions | |
| */ | |
| public $useMultibyteStringFunctions; | |
| /** | |
| * Constructor | |
| * | |
| * @param string $className Name of the class to be created | |
| * @param string $filepath If set, path to the file where the renderer will be saved | |
| * @return void | |
| */ | |
| public function __construct($cacheDir = null) | |
| { | |
| if (isset($cacheDir)) | |
| { | |
| $this->cacheDir = $cacheDir; | |
| } | |
| if (extension_loaded('tokenizer')) | |
| { | |
| $this->optimizer = new Optimizer; | |
| } | |
| $this->useMultibyteStringFunctions = extension_loaded('mbstring'); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getRenderer(Stylesheet $stylesheet) | |
| { | |
| // Generate the source file | |
| $php = $this->generate($stylesheet->get()); | |
| // Save the file if applicable | |
| if (isset($this->filepath)) | |
| { | |
| $filepath = $this->filepath; | |
| } | |
| elseif (isset($this->cacheDir)) | |
| { | |
| $filepath = $this->cacheDir . '/' . str_replace('\\', '_', $this->lastClassName) . '.php'; | |
| } | |
| if (isset($filepath)) | |
| { | |
| file_put_contents($filepath, "<?php\n" . $php); | |
| $this->lastFilepath = realpath($filepath); | |
| } | |
| // Execute the source to create the class if it doesn't exist | |
| if (!class_exists($this->lastClassName, false)) | |
| { | |
| eval($php); | |
| } | |
| // Create an instance and copy the source into the instance | |
| $renderer = new $this->lastClassName; | |
| $renderer->source = $php; | |
| return $renderer; | |
| } | |
| /** | |
| * Generate the source for a PHP class that renders an intermediate representation according to | |
| * given stylesheet | |
| * | |
| * @param string $xsl XSL stylesheet | |
| * @return string | |
| */ | |
| public function generate($xsl) | |
| { | |
| $header = "/**\n" | |
| . "* @package s9e\TextFormatter\n" | |
| . "* @copyright Copyright (c) 2010-2013 The s9e Authors\n" | |
| . "* @license http://www.opensource.org/licenses/mit-license.php The MIT License\n" | |
| . "*/\n\n"; | |
| // Parse the stylesheet | |
| $ir = TemplateParser::parse($xsl); | |
| $xpath = new DOMXPath($ir); | |
| // Set the output method | |
| $this->outputMethod = $ir->documentElement->getAttribute('outputMethod'); | |
| // Apply the empty-element options | |
| $this->fixEmptyElements($ir); | |
| // Generate the arrays of parameters, sorted by whether they are static or dynamic | |
| $dynamicParams = []; | |
| $staticParams = []; | |
| foreach ($ir->getElementsByTagName('param') as $param) | |
| { | |
| $paramName = $param->getAttribute('name'); | |
| $paramValue = ($param->hasAttribute('select')) ? $param->getAttribute('select') : "''"; | |
| // Test whether the param value is a literal | |
| if (preg_match('#^(?:\'[^\']*\'|"[^"]*"|[0-9]+)$#', $paramValue)) | |
| { | |
| $staticParams[] = var_export($paramName, true) . '=>' . $paramValue; | |
| } | |
| else | |
| { | |
| $dynamicParams[] = var_export($paramName, true) . '=>' . var_export($paramValue, true); | |
| } | |
| } | |
| // Start the code right after the class name, we'll prepend the header when we're done | |
| $this->php = ' extends \\s9e\\TextFormatter\\Renderer | |
| { | |
| protected $htmlOutput=' . var_export($this->outputMethod === 'html', true) . '; | |
| protected $dynamicParams=[' . implode(',', $dynamicParams) . ']; | |
| protected $params=[' . implode(',', $staticParams) . ']; | |
| protected $xpath; | |
| public function __sleep() | |
| { | |
| $props = get_object_vars($this); | |
| unset($props["out"], $props["proc"], $props["source"], $props["xpath"]); | |
| return array_keys($props); | |
| } | |
| public function setParameter($paramName, $paramValue) | |
| { | |
| $this->params[$paramName] = (string) $paramValue; | |
| unset($this->dynamicParams[$paramName]); | |
| } | |
| public function renderRichText($xml) | |
| { | |
| $dom = $this->loadXML($xml); | |
| $this->xpath = new \\DOMXPath($dom); | |
| $this->out = "";'; | |
| if ($dynamicParams) | |
| { | |
| $this->php .= ' | |
| foreach ($this->dynamicParams as $k => $v) | |
| { | |
| $this->params[$k] = $this->xpath->evaluate("string($v)", $dom); | |
| }'; | |
| } | |
| if ($xpath->evaluate('count(//applyTemplates[@select])')) | |
| { | |
| $nodesPHP = '(isset($xpath)) ? $this->xpath->query($xpath, $root) : $root->childNodes'; | |
| } | |
| else | |
| { | |
| $nodesPHP = '$root->childNodes'; | |
| } | |
| $this->php .= ' | |
| $this->at($dom->documentElement); | |
| unset($this->xpath); | |
| return $this->out; | |
| } | |
| protected function at($root, $xpath = null) | |
| { | |
| if ($root->nodeType === 3) | |
| { | |
| $this->out .= htmlspecialchars($root->textContent,' . ENT_NOQUOTES . '); | |
| } | |
| else | |
| { | |
| foreach (' . $nodesPHP . ' as $node) | |
| { | |
| $nodeName = $node->nodeName;'; | |
| // Remove the excess indentation | |
| $this->php = str_replace("\n\t\t\t", "\n", $this->php); | |
| // Collect and sort templates | |
| $templates = []; | |
| foreach ($ir->getElementsByTagName('template') as $template) | |
| { | |
| // Parse this template and save its internal representation | |
| $irXML = $template->ownerDocument->saveXML($template); | |
| // Get the template's match values | |
| foreach ($template->getElementsByTagName('match') as $match) | |
| { | |
| $expr = $match->textContent; | |
| $priority = $match->getAttribute('priority'); | |
| // Separate the tagName from the predicate, if any | |
| if (preg_match('#^(\\w+)\\[(.*)\\]$#s', $expr, $m)) | |
| { | |
| $tagName = $m[1]; | |
| $predicate = $m[2]; | |
| } | |
| else | |
| { | |
| $tagName = $expr; | |
| $predicate = ''; | |
| } | |
| // Test whether this is a wildcard template | |
| if (preg_match('#^(\\w+):\\*#', $tagName, $m)) | |
| { | |
| $condition = '$node->prefix===' . var_export($m[1], true); | |
| } | |
| else | |
| { | |
| $condition = '$nodeName===' . var_export($tagName, true); | |
| } | |
| // Add the predicate to the condition | |
| if ($predicate !== '') | |
| { | |
| $condition = '(' . $condition . '&&' . $this->convertCondition($predicate) . ')'; | |
| } | |
| // Record this template | |
| $templates[$priority][$irXML][] = $condition; | |
| } | |
| } | |
| // Sort templates by priority descending | |
| krsort($templates); | |
| // Build the big if/else structure | |
| $else = ''; | |
| foreach ($templates as $groupedTemplates) | |
| { | |
| // Process the grouped templates in reverse order so that the last templates apply first | |
| // to match XSLT's default behaviour | |
| foreach (array_reverse($groupedTemplates) as $irXML => $conditions) | |
| { | |
| $ir = new DOMDocument; | |
| $ir->loadXML($irXML); | |
| $this->php .= $else; | |
| $else = 'else'; | |
| // If there's only one condition, remove its parentheses if applicable | |
| if (count($conditions) === 1 | |
| && $conditions[0][0] === '(' | |
| && substr($conditions[0], -1) === ')') | |
| { | |
| $conditions[0] = substr($conditions[0], 1, -1); | |
| } | |
| $this->php .= 'if(' . implode('||', $conditions) . ')'; | |
| $this->php .= '{'; | |
| $this->serializeChildren($ir->documentElement); | |
| $this->php .= '}'; | |
| } | |
| } | |
| // Add the default handling and close the method | |
| $this->php .= "else \$this->at(\$node);\n\t\t\t}\n\t\t}\n\t}"; | |
| // Add the getParamAsXPath() method if necessary | |
| if (strpos($this->php, '$this->getParamAsXPath(') !== false) | |
| { | |
| $this->php .= str_replace( | |
| "\n\t\t\t\t", | |
| "\n", | |
| <<<'EOT' | |
| protected function getParamAsXPath($k) | |
| { | |
| if (isset($this->dynamicParams[$k])) | |
| { | |
| return $this->dynamicParams[$k]; | |
| } | |
| if (!isset($this->params[$k])) | |
| { | |
| return "''"; | |
| } | |
| $str = $this->params[$k]; | |
| if (strpos($str, "'") === false) | |
| { | |
| return "'" . $str . "'"; | |
| } | |
| if (strpos($str, '"') === false) | |
| { | |
| return '"' . $str . '"'; | |
| } | |
| $toks = []; | |
| $c = '"'; | |
| $pos = 0; | |
| while ($pos < strlen($str)) | |
| { | |
| $spn = strcspn($str, $c, $pos); | |
| if ($spn) | |
| { | |
| $toks[] = $c . substr($str, $pos, $spn) . $c; | |
| $pos += $spn; | |
| } | |
| $c = ($c === '"') ? "'" : '"'; | |
| } | |
| return 'concat(' . implode(',', $toks) . ')'; | |
| } | |
| EOT | |
| ); | |
| } | |
| // Remove the references to $this->xpath if it's never used | |
| if (strpos($this->php, '$this->xpath->') === false) | |
| { | |
| $this->php = preg_replace( | |
| [ | |
| '#\\s*\\$this->xpath\\s*=.*#', | |
| '#\\s*unset\\(\\$this->xpath\\);#' | |
| ], | |
| '', | |
| $this->php | |
| ); | |
| } | |
| // Close the class definition | |
| $this->php .= "\n}"; | |
| // Generate a name for that class if necessary, and save it | |
| $className = (isset($this->className)) | |
| ? $this->className | |
| : $this->defaultClassPrefix . sha1($this->php); | |
| $this->lastClassName = $className; | |
| // Declare the namespace and class name | |
| $pos = strrpos($className, '\\'); | |
| if ($pos !== false) | |
| { | |
| $header .= 'namespace ' . substr($className, 0, $pos) . ";\n\n"; | |
| $className = substr($className, 1 + $pos); | |
| } | |
| // Prepend the header and the class name | |
| $this->php = $header . 'class ' . $className . $this->php; | |
| // Optimize the generated code | |
| if (isset($this->optimizer)) | |
| { | |
| $this->php = $this->optimizer->optimize($this->php); | |
| } | |
| return $this->php; | |
| } | |
| /** | |
| * Change the IR to respect the empty-element options | |
| * | |
| * @param DOMNode $ir | |
| * @return void | |
| */ | |
| protected function fixEmptyElements(DOMNode $ir) | |
| { | |
| if ($this->outputMethod !== 'xml') | |
| { | |
| return; | |
| } | |
| foreach ($ir->getElementsByTagName('element') as $element) | |
| { | |
| $isEmpty = $element->getAttribute('empty'); | |
| $isVoid = $element->getAttribute('void'); | |
| if ($isVoid || $isEmpty === 'no') | |
| { | |
| continue; | |
| } | |
| if (!$this->useEmptyElements) | |
| { | |
| $element->setAttribute('empty', 'no'); | |
| } | |
| elseif ($isEmpty === 'maybe' && !$this->forceEmptyElements) | |
| { | |
| $element->setAttribute('empty', 'no'); | |
| } | |
| } | |
| } | |
| //========================================================================== | |
| // Serialization of the internal representation into PHP | |
| //========================================================================== | |
| /** | |
| * Serialize an <applyTemplates/> node | |
| * | |
| * @param DOMNode $applyTemplates <applyTemplates/> node | |
| * @return void | |
| */ | |
| protected function serializeApplyTemplates(DOMNode $applyTemplates) | |
| { | |
| $this->php .= '$this->at($node'; | |
| if ($applyTemplates->hasAttribute('select')) | |
| { | |
| $this->php .= ',' . var_export($applyTemplates->getAttribute('select'), true); | |
| } | |
| $this->php .= ');'; | |
| } | |
| /** | |
| * Serialize an <attribute/> node | |
| * | |
| * @param DOMNode $attribute <attribute/> node | |
| * @return void | |
| */ | |
| protected function serializeAttribute(DOMNode $attribute) | |
| { | |
| $attrName = $attribute->getAttribute('name'); | |
| // PHP representation of this attribute's name | |
| $phpAttrName = $this->convertAttributeValueTemplate($attrName); | |
| // NOTE: the attribute name is escaped by default to account for dynamically-generated names | |
| $phpAttrName = 'htmlspecialchars(' . $phpAttrName . ',' . ENT_QUOTES . ')'; | |
| $this->php .= "\$this->out.=' '." . $phpAttrName . ".'=\"';"; | |
| $this->serializeChildren($attribute); | |
| $this->php .= "\$this->out.='\"';"; | |
| } | |
| /** | |
| * Serialize all the children of given node into PHP | |
| * | |
| * @param DOMNode $ir Internal representation | |
| * @return void | |
| */ | |
| protected function serializeChildren(DOMNode $ir) | |
| { | |
| foreach ($ir->childNodes as $node) | |
| { | |
| $methodName = 'serialize' . ucfirst($node->localName); | |
| $this->$methodName($node); | |
| } | |
| } | |
| /** | |
| * Serialize a <closeTag/> node | |
| * | |
| * @param DOMNode $closeTag <closeTag/> node | |
| * @return void | |
| */ | |
| protected function serializeCloseTag(DOMNode $closeTag) | |
| { | |
| $id = $closeTag->getAttribute('id'); | |
| if ($closeTag->hasAttribute('check')) | |
| { | |
| $this->php .= 'if(!isset($t' . $id . ')){'; | |
| } | |
| if ($closeTag->hasAttribute('set')) | |
| { | |
| $this->php .= '$t' . $id . '=1;'; | |
| } | |
| // Get the element that's being closed | |
| $xpath = new DOMXPath($closeTag->ownerDocument); | |
| $element = $xpath->query('ancestor::element[@id="' . $id . '"]', $closeTag)->item(0); | |
| $isVoid = $element->getAttribute('void'); | |
| $isEmpty = $element->getAttribute('empty'); | |
| if ($this->outputMethod === 'html') | |
| { | |
| $this->php .= "\$this->out.='>';"; | |
| if ($isVoid === 'maybe') | |
| { | |
| // Check at runtime whether this element is not void | |
| $this->php .= 'if(!$v' . $id . '){'; | |
| } | |
| } | |
| else | |
| { | |
| // In XML mode, we only care about whether this element is empty | |
| if ($isEmpty === 'yes') | |
| { | |
| // Definitely empty, use a self-closing tag | |
| $this->php .= "\$this->out.='/>';"; | |
| } | |
| else | |
| { | |
| // Since it's not definitely empty, we'll close this start tag normally | |
| $this->php .= "\$this->out.='>';"; | |
| if ($isEmpty === 'maybe') | |
| { | |
| // Maybe empty, record the length of the output and if it doesn't grow we'll | |
| // change the start tag into a self-closing tag | |
| $this->php .= '$l' . $id . '=strlen($this->out);'; | |
| } | |
| } | |
| } | |
| if ($closeTag->hasAttribute('check')) | |
| { | |
| $this->php .= '}'; | |
| } | |
| } | |
| /** | |
| * Serialize a <comment/> node | |
| * | |
| * @param DOMNode $comment <comment/> node | |
| * @return void | |
| */ | |
| protected function serializeComment(DOMNode $comment) | |
| { | |
| $this->php .= "\$this->out.='<!--';"; | |
| $this->serializeChildren($comment); | |
| $this->php .= "\$this->out.='-->';"; | |
| } | |
| /** | |
| * Serialize a <copyOfAttributes/> node | |
| * | |
| * @param DOMNode $copyOfAttributes <copyOfAttributes/> node | |
| * @return void | |
| */ | |
| protected function serializeCopyOfAttributes(DOMNode $copyOfAttributes) | |
| { | |
| $this->php .= 'foreach($node->attributes as $attribute)'; | |
| $this->php .= '{'; | |
| $this->php .= '$this->out.=\' \';'; | |
| $this->php .= '$this->out.=$attribute->name;'; | |
| $this->php .= '$this->out.=\'="\';'; | |
| $this->php .= '$this->out.=htmlspecialchars($attribute->value,' . ENT_COMPAT . ');'; | |
| $this->php .= '$this->out.=\'"\';'; | |
| $this->php .= '}'; | |
| } | |
| /** | |
| * Serialize an <element/> node | |
| * | |
| * @param DOMNode $element <element/> node | |
| * @return void | |
| */ | |
| protected function serializeElement(DOMNode $element) | |
| { | |
| $elName = $element->getAttribute('name'); | |
| $id = $element->getAttribute('id'); | |
| $isVoid = $element->getAttribute('void'); | |
| $isEmpty = $element->getAttribute('empty'); | |
| // Test whether this element name is dynamic | |
| $isDynamic = (bool) (strpos($elName, '{') !== false); | |
| // PHP representation of this element's name | |
| $phpElName = $this->convertAttributeValueTemplate($elName); | |
| // NOTE: the element name is escaped by default to account for dynamically-generated names | |
| $phpElName = 'htmlspecialchars(' . $phpElName . ',' . ENT_QUOTES . ')'; | |
| // If the element name is dynamic, we cache its name for convenience and performance | |
| if ($isDynamic) | |
| { | |
| $varName = '$e' . $id; | |
| // Add the var declaration to the source | |
| $this->php .= $varName . '=' . $phpElName . ';'; | |
| // Replace the element name with the var | |
| $phpElName = $varName; | |
| } | |
| // Test whether this element is void if we need this information | |
| if ($this->outputMethod === 'html' && $isVoid === 'maybe') | |
| { | |
| $this->php .= '$v' . $id . '=preg_match(' . var_export(TemplateParser::$voidRegexp, true) . ',' . $phpElName . ');'; | |
| } | |
| // Open the start tag | |
| $this->php .= "\$this->out.='<'." . $phpElName . ';'; | |
| // Serialize this element's content | |
| $this->serializeChildren($element); | |
| // If we're in XML mode and the element is or may be empty, we may not need to close it at | |
| // all | |
| if ($this->outputMethod === 'xml') | |
| { | |
| // If this element is definitely empty, it has already been closed with a self-closing | |
| // tag in serializeCloseTag() | |
| if ($isEmpty === 'yes') | |
| { | |
| return; | |
| } | |
| // If this element may be empty, we need to check at runtime whether we turn its start | |
| // tag into a self-closing tag or append an end tag | |
| if ($isEmpty === 'maybe') | |
| { | |
| $this->php .= 'if($l' . $id . '===strlen($this->out)){'; | |
| $this->php .= "\$this->out=substr(\$this->out,0,-1).'/>';"; | |
| $this->php .= '}else{'; | |
| $this->php .= "\$this->out.='</'." . $phpElName . ".'>';"; | |
| $this->php .= '}'; | |
| return; | |
| } | |
| } | |
| // Close that element, unless we're in HTML mode and we know it's void | |
| if ($this->outputMethod !== 'html' || $isVoid !== 'yes') | |
| { | |
| $this->php .= "\$this->out.='</'." . $phpElName . ".'>';"; | |
| } | |
| // If this element was maybe void, serializeCloseTag() has put its content within an if | |
| // block. We need to close that block | |
| if ($this->outputMethod === 'html' && $isVoid === 'maybe') | |
| { | |
| $this->php .= '}'; | |
| } | |
| } | |
| /** | |
| * Unused | |
| */ | |
| protected function serializeMatch() | |
| { | |
| } | |
| /** | |
| * Serialize an <output/> node | |
| * | |
| * @param DOMNode $output <output/> node | |
| * @return void | |
| */ | |
| protected function serializeOutput(DOMNode $output) | |
| { | |
| $xpath = new DOMXPath($output->ownerDocument); | |
| $escapeMode = ($xpath->evaluate('count(ancestor::attribute)', $output)) | |
| ? ENT_COMPAT | |
| : ENT_NOQUOTES; | |
| if ($output->getAttribute('type') === 'xpath') | |
| { | |
| $this->php .= '$this->out.=htmlspecialchars('; | |
| $this->php .= $this->convertXPath($output->textContent); | |
| $this->php .= ',' . $escapeMode . ');'; | |
| } | |
| else | |
| { | |
| // Literal | |
| $this->php .= '$this->out.='; | |
| $this->php .= var_export(htmlspecialchars($output->textContent, $escapeMode), true); | |
| $this->php .= ';'; | |
| } | |
| } | |
| /** | |
| * Serialize a <switch/> node | |
| * | |
| * @param DOMNode $switch <switch/> node | |
| * @return void | |
| */ | |
| protected function serializeSwitch(DOMNode $switch) | |
| { | |
| $else = ''; | |
| foreach ($switch->getElementsByTagName('case') as $case) | |
| { | |
| if ($case->parentNode !== $switch) | |
| { | |
| continue; | |
| } | |
| if ($case->hasAttribute('test')) | |
| { | |
| $this->php .= $else . 'if(' . $this->convertCondition($case->getAttribute('test')) . ')'; | |
| } | |
| else | |
| { | |
| $this->php .= 'else'; | |
| } | |
| $else = 'else'; | |
| $this->php .= '{'; | |
| $this->serializeChildren($case); | |
| $this->php .= '}'; | |
| } | |
| } | |
| //========================================================================== | |
| // XPath conversion | |
| //========================================================================== | |
| /** | |
| * Convert an attribute value template into PHP | |
| * | |
| * NOTE: escaping must be performed by the caller | |
| * | |
| * @link http://www.w3.org/TR/xslt#dt-attribute-value-template | |
| * | |
| * @param string $attrValue Attribute value template | |
| * @return void | |
| */ | |
| protected function convertAttributeValueTemplate($attrValue) | |
| { | |
| $phpExpressions = []; | |
| foreach (TemplateHelper::parseAttributeValueTemplate($attrValue) as $token) | |
| { | |
| if ($token[0] === 'literal') | |
| { | |
| $phpExpressions[] = var_export($token[1], true); | |
| } | |
| else | |
| { | |
| $phpExpressions[] = $this->convertXPath($token[1]); | |
| } | |
| } | |
| return implode('.', $phpExpressions); | |
| } | |
| /** | |
| * Convert an XPath condition into a PHP condition | |
| * | |
| * This method is similar to convertXPath() but it selectively replaces some simple conditions | |
| * with the corresponding DOM method for performance reasons | |
| * | |
| * @param string $expr XPath expression | |
| * @return string PHP code | |
| */ | |
| protected function convertCondition($expr) | |
| { | |
| $expr = trim($expr); | |
| // <xsl:if test="@foo"> | |
| // if ($node->hasAttribute('foo')) | |
| if (preg_match('#^@([-\\w]+)$#', $expr, $m)) | |
| { | |
| return '$node->hasAttribute(' . var_export($m[1], true) . ')'; | |
| } | |
| // <xsl:if test="not(@foo)"> | |
| // if (!$node->hasAttribute('foo')) | |
| if (preg_match('#^not\\(@([-\\w]+)\\)$#', $expr, $m)) | |
| { | |
| return '!$node->hasAttribute(' . var_export($m[1], true) . ')'; | |
| } | |
| // <xsl:if test="$foo"> | |
| // if (!empty($this->params['foo'])) | |
| if (preg_match('#^\\$(\\w+)$#', $expr, $m)) | |
| { | |
| return '!empty($this->params[' . var_export($m[1], true) . '])'; | |
| } | |
| // <xsl:if test="not($foo)"> | |
| // if (empty($this->params['foo'])) | |
| if (preg_match('#^not\\(\\$(\\w+)\\)$#', $expr, $m)) | |
| { | |
| return 'empty($this->params[' . var_export($m[1], true) . '])'; | |
| } | |
| // If the condition does not seem to contain a relational expression, or start with a | |
| // function call, we wrap it inside of a boolean() call | |
| if (!preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr)) | |
| { | |
| // <xsl:if test="parent::foo"> | |
| // if ($this->xpath->evaluate("boolean(parent::foo)",$node)) | |
| $expr = 'boolean(' . $expr . ')'; | |
| } | |
| // <xsl:if test="@foo='bar'"> | |
| // if ($this->xpath->evaluate("@foo='bar'",$node)) | |
| return $this->convertXPath($expr); | |
| } | |
| /** | |
| * Convert an XPath expression into PHP code | |
| * | |
| * @param string $expr XPath expression | |
| * @return string PHP code | |
| */ | |
| protected function convertXPath($expr) | |
| { | |
| static $regexp; | |
| $expr = trim($expr); | |
| // Use the custom representation if applicable | |
| if (isset($this->customXPath[$expr])) | |
| { | |
| return $this->customXPath[$expr]; | |
| } | |
| if (!isset($regexp)) | |
| { | |
| $patterns = [ | |
| 'attr' => ['@', '(?<attrName>[-\\w]+)'], | |
| 'dot' => '\\.', | |
| 'not' => ['not', '\\(', '(?&value)', '\\)'], | |
| 'name' => 'name\\(\\)', | |
| 'lname' => 'local-name\\(\\)', | |
| 'param' => ['\\$', '(?<paramName>\\w+)'], | |
| 'string' => '"[^"]*"|\'[^\']*\'', | |
| 'number' => ['-?', '\\d++'], | |
| 'strlen' => ['string-length', '\\(', '(?<strlen0>(?&value))?', '\\)'], | |
| 'contains' => [ | |
| 'contains', | |
| '\\(', | |
| '(?<contains0>(?&value))', | |
| ',', | |
| '(?<contains1>(?&value))', | |
| '\\)' | |
| ], | |
| 'translate' => [ | |
| 'translate', | |
| '\\(', | |
| '(?<translate0>(?&value))', | |
| ',', | |
| '(?<translate1>(?&string))', | |
| ',', | |
| '(?<translate2>(?&string))', | |
| '\\)' | |
| ], | |
| 'substr' => [ | |
| 'substring', | |
| '\\(', | |
| '(?<substr0>(?&value))', | |
| ',', | |
| '(?<substr1>(?&value))', | |
| '(?:, (?<substr2>(?&value)))?', | |
| '\\)' | |
| ] | |
| ]; | |
| $exprs = []; | |
| // Create a regexp that matches values, such as "@foo" or "42" | |
| $valueExprs = []; | |
| foreach ($patterns as $name => $pattern) | |
| { | |
| if (is_array($pattern)) | |
| { | |
| $pattern = implode(' ', $pattern); | |
| } | |
| $valueExprs[] = '(?<' . $name . '>' . $pattern . ')'; | |
| } | |
| $exprs[] = '(?<value>' . implode('|', $valueExprs) . ')'; | |
| // Create a regexp that matches a comparison such as "@foo = 1" | |
| // NOTE: cannot support < or > because of NaN -- (@foo<5) returns false if @foo='' | |
| $exprs[] = '(?<cmp>(?<cmp0>(?&value)) (?<cmp1>!?=) (?<cmp2>(?&value)))'; | |
| // Match parenthesized expressions on PCRE >= 8.13, previous versions segfault | |
| // because of the mutual references | |
| $parensMatch = ''; | |
| if (version_compare(PCRE_VERSION, '8.13', '>=')) | |
| { | |
| $parensMatch = '|(?&parens)'; | |
| // Create a regexp that matches a parenthesized expression | |
| // NOTE: could be expanded to support any expression | |
| $exprs[] = '(?<parens>\\( (?<parens0>(?&bool)|(?&cmp)) \\))'; | |
| } | |
| // Create a regexp that matches boolean operations | |
| $exprs[] = '(?<bool>(?<bool0>(?&cmp)|(?&value)' . $parensMatch . ') (?<bool1>and|or) (?<bool2>(?&cmp)|(?&value)|(?&bool)' . $parensMatch . '))'; | |
| // Assemble the final regexp | |
| $regexp = '#^(?:' . implode('|', $exprs) . ')$#S'; | |
| // Replace spaces with any amount of whitespace | |
| $regexp = str_replace(' ', '\\s*', $regexp); | |
| } | |
| if (preg_match($regexp, $expr, $m)) | |
| { | |
| if (!empty($m['attrName'])) | |
| { | |
| // <xsl:value-of select="@foo"/> | |
| // $this->out .= $node->getAttribute('foo'); | |
| return '$node->getAttribute(' . var_export($m['attrName'], true) . ')'; | |
| } | |
| // <xsl:value-of select="."/> | |
| // $this->out .= $node->textContent; | |
| if (!empty($m['dot'])) | |
| { | |
| return '$node->textContent'; | |
| } | |
| // <xsl:value-of select="$foo"/> | |
| // $this->out .= $this->params['foo']; | |
| if (!empty($m['paramName'])) | |
| { | |
| return '$this->params[' . var_export($m['paramName'], true) . ']'; | |
| } | |
| // <xsl:value-of select="'foo'"/> | |
| // <xsl:value-of select='"foo"'/> | |
| // $this->out .= 'foo'; | |
| if (!empty($m['string'])) | |
| { | |
| return var_export(substr($m['string'], 1, -1), true); | |
| } | |
| // <xsl:value-of select="local-name()"/> | |
| // $this->out .= $node->localName; | |
| if (!empty($m['lname'])) | |
| { | |
| return '$node->localName'; | |
| } | |
| // <xsl:value-of select="name()"/> | |
| // $this->out .= $node->nodeName; | |
| if (!empty($m['name'])) | |
| { | |
| return '$node->nodeName'; | |
| } | |
| // <xsl:value-of select="3"/> | |
| // $this->out .= '3'; | |
| if (!empty($m['number'])) | |
| { | |
| return "'" . $expr . "'"; | |
| } | |
| // <xsl:value-of select="string-length(@foo)"/> | |
| // $this->out .= mb_strlen($node->getAttribute('foo'),'utf-8'); | |
| if (!empty($m['strlen']) && $this->useMultibyteStringFunctions) | |
| { | |
| if (!isset($m['strlen0'])) | |
| { | |
| $m['strlen0'] = '.'; | |
| } | |
| return 'mb_strlen(' . $this->convertXPath($m['strlen0']) . ",'utf-8')"; | |
| } | |
| // <xsl:value-of select="substring(@foo, 1, 2)"/> | |
| // $this->out .= mb_substring($node->getAttribute('foo'),0,2,'utf-8'); | |
| // | |
| // NOTE: negative values for the second argument do not produce the same result as | |
| // specified in XPath if the argument is not a literal number | |
| if (!empty($m['substr']) && $this->useMultibyteStringFunctions) | |
| { | |
| $php = 'mb_substr(' . $this->convertXPath($m['substr0']) . ','; | |
| // Hardcode the value if possible | |
| if (preg_match('#^\\d+$#D', $m['substr1'])) | |
| { | |
| $php .= max(0, $m['substr1'] - 1); | |
| } | |
| else | |
| { | |
| $php .= 'max(0,' . $this->convertXPath($m['substr1']) . '-1)'; | |
| } | |
| $php .= ','; | |
| if (isset($m['substr2'])) | |
| { | |
| if (preg_match('#^\\d+$#D', $m['substr2'])) | |
| { | |
| // Handles substring(0,2) as per XPath | |
| if (preg_match('#^\\d+$#D', $m['substr1']) && $m['substr1'] < 1) | |
| { | |
| $php .= max(0, $m['substr1'] + $m['substr2'] - 1); | |
| } | |
| else | |
| { | |
| $php .= max(0, $m['substr2']); | |
| } | |
| } | |
| else | |
| { | |
| $php .= 'max(0,' . $this->convertXPath($m['substr2']) . ')'; | |
| } | |
| } | |
| else | |
| { | |
| $php .= 'null'; | |
| } | |
| $php .= ",'utf-8')"; | |
| return $php; | |
| } | |
| if (!empty($m['contains'])) | |
| { | |
| return '(strpos(' . $this->convertXPath($m['contains0']) . ',' . $this->convertXPath($m['contains1']) . ')!==false)'; | |
| } | |
| if (!empty($m['cmp1'])) | |
| { | |
| $operators = [ | |
| '=' => '===', | |
| '!=' => '!==', | |
| '>' => '>', | |
| '>=' => '>=', | |
| '<' => '<', | |
| '<=' => '<=' | |
| ]; | |
| // If either operand is a number, represent it as a PHP number and replace the | |
| // identity operators | |
| foreach (['cmp0', 'cmp2'] as $k) | |
| { | |
| if (preg_match('#^\\d+$#', $m[$k])) | |
| { | |
| $operators['='] = '=='; | |
| $operators['!='] = '!='; | |
| $m[$k] = ltrim($m[$k], '0'); | |
| } | |
| else | |
| { | |
| $m[$k] = $this->convertXPath($m[$k]); | |
| } | |
| } | |
| return $m['cmp0'] . $operators[$m['cmp1']] . $m['cmp2']; | |
| } | |
| if (!empty($m['bool1'])) | |
| { | |
| $operators = [ | |
| 'and' => '&&', | |
| 'or' => '||' | |
| ]; | |
| return $this->convertCondition($m['bool0']) . $operators[$m['bool1']] . $this->convertCondition($m['bool2']); | |
| } | |
| if (!empty($m['parens'])) | |
| { | |
| return '(' . $this->convertXPath($m['parens0']) . ')'; | |
| } | |
| if (!empty($m['translate'])) | |
| { | |
| preg_match_all('/./u', substr($m['translate1'], 1, -1), $matches); | |
| $from = $matches[0]; | |
| preg_match_all('/./u', substr($m['translate2'], 1, -1), $matches); | |
| $to = $matches[0]; | |
| // We adjust $to to match the number of elements in $from, either by truncating it | |
| // or by padding it with empty strings | |
| if (count($to) > count($from)) | |
| { | |
| $to = array_slice($to, 0, count($from)); | |
| } | |
| else | |
| { | |
| // NOTE: we don't use array_merge() because of potential side-effects when | |
| // translating digits | |
| while (count($from) > count($to)) | |
| { | |
| $to[] = ''; | |
| } | |
| } | |
| // Remove duplicates in $from, as well as the corresponding elements in $to | |
| $from = array_unique($from); | |
| $to = array_intersect_key($to, $from); | |
| // Start building the strtr() call | |
| $php = 'strtr(' . $this->convertXPath($m['translate0']) . ','; | |
| // Test whether all elements in $from and $to are exactly 1 byte long, meaning they | |
| // are ASCII and with no empty strings. If so, we can use the scalar version of | |
| // strtr(), otherwise we have to use the array version | |
| if ([1] === array_unique(array_map('strlen', $from)) | |
| && [1] === array_unique(array_map('strlen', $to))) | |
| { | |
| $php .= var_export(implode('', $from), true) . ',' . var_export(implode('', $to), true); | |
| } | |
| else | |
| { | |
| $php .= '['; | |
| $cnt = count($from); | |
| for ($i = 0; $i < $cnt; ++$i) | |
| { | |
| if ($i) | |
| { | |
| $php .= ','; | |
| } | |
| $php .= var_export($from[$i], true) . '=>' . var_export($to[$i], true); | |
| } | |
| $php .= ']'; | |
| } | |
| $php .= ')'; | |
| return $php; | |
| } | |
| } | |
| // If the condition does not seem to contain a relational expression, or start with a | |
| // function call, we wrap it inside of a string() call | |
| if (!preg_match('#[=<>]|\\bor\\b|\\band\\b|^[-\\w]+\\s*\\(#', $expr)) | |
| { | |
| $expr = 'string(' . $expr . ')'; | |
| } | |
| // Parse the expression for variables | |
| $phpTokens = []; | |
| $pos = 0; | |
| $len = strlen($expr); | |
| while ($pos < $len) | |
| { | |
| // If we have a string literal, capture it and add its PHP representation | |
| if ($expr[$pos] === "'" || $expr[$pos] === '"') | |
| { | |
| $nextPos = strpos($expr, $expr[$pos], 1 + $pos); | |
| if ($nextPos === false) | |
| { | |
| throw new RuntimeException('Unterminated string literal in XPath expression ' . var_export($expr, true)); | |
| } | |
| // Capture the string | |
| $phpTokens[] = var_export(substr($expr, $pos, $nextPos + 1 - $pos), true); | |
| // Move the cursor past the string | |
| $pos = $nextPos + 1; | |
| continue; | |
| } | |
| // Variables in XPath expressions have to be resolved at runtime via getParamAsXPath() | |
| if ($expr[$pos] === '$' && preg_match('/\\$(\\w+)/', $expr, $m, 0, $pos)) | |
| { | |
| $phpTokens[] = '$this->getParamAsXPath(' . var_export($m[1], true) . ')'; | |
| $pos += strlen($m[0]); | |
| continue; | |
| } | |
| // Capture everything up to the next interesting character | |
| $spn = strcspn($expr, '\'"$', $pos); | |
| if ($spn) | |
| { | |
| $phpTokens[] = var_export(substr($expr, $pos, $spn), true); | |
| $pos += $spn; | |
| } | |
| } | |
| return '$this->xpath->evaluate(' . implode('.', $phpTokens) . ',$node)'; | |
| } |